﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FreshMeat.Threading;

namespace FreshMeat.Storage
{
   /// <summary>
   /// Context for a <see cref="FileStore{TId,TData}"/>.
   /// </summary>
   /// <typeparam name="TId">Type of ID.</typeparam>
   /// <typeparam name="TData">Type of data.</typeparam>
   public class FileStoreContext<TId, TData> : StoreContext, IStoreContext<TId, TData>
   {
      readonly FileLocator<TId> locator;
      readonly ISerializer<TData> serializer;

      /// <summary>
      /// Creates instance.
      /// </summary>
      /// <param name="locator"></param>
      /// <param name="serializer"></param>
      /// <param name="resourceLock"></param>
      public FileStoreContext(FileLocator<TId> locator, ISerializer<TData> serializer, ILock resourceLock)
         : base(resourceLock)
      {
         this.locator = locator;
         this.serializer = serializer;
      }

      /// <summary>
      /// Saves data to the store.
      /// </summary>
      /// <param name="id">The ID.</param>
      /// <param name="data">The data.</param>
      /// <returns>True if the item already existed; false if it was created.</returns>
      public bool Save(TId id, TData data)
      {
         Lock();

         // A key objective is that if something goes wrong while writing
         // the new data then the previous data should be retained.
         // 
         // The deletion of a file may not result in its immediate removal
         // (definitely an issue in Windows, may be in other systems, too).
         // The workaround is to rename the file and then delete the renamed
         // file so the original name will behave as if the file doesn't exist.

         // Do serialization first to get those errors out of the way.
         // If this fails there's nothing to undo since we haven't changed any files.
         var serializedData = serializer.Serialize(data);

         // Get the path to the file.
         var file = locator.GetPath(id);

         // Write the data to a temp file, overwriting if it already exists.
         // If this fails there's nothing to undo since the real file is unchanged.
         var temp = locator.GetTempName(file);
         locator.FileSystem.WriteAllText(temp, serializedData);
         var deleteTemp = true;
         try
         {
            // If the real file already exists then rename it as a backup.
            // If this fails there's nothing to undo since the rename failed so the real file should be unchanged.
            string backup = null;
            var exists = locator.FileSystem.FileExists(file);
            if (exists)
            {
               backup = locator.GetTempName(file);
               locator.FileSystem.MoveFile(file, backup);
            }

            try
            {
               // Rename the temp file to become the real file.
               locator.FileSystem.MoveFile(temp, file);

               deleteTemp = false; // if we get here then there's no need to delete the temp file
            }
            catch (Exception)
            {
               // Rename the backup file to become the real file again.
               // If this fails then the store would be left in an inconsistent state.
               // This really *should* never happen because if we were able to rename the
               // original to the backup then we should be able to rename it back again.
               if (exists)
                  locator.FileSystem.MoveFile(backup, file);

               // We've restored the original but still need to report the error
               throw;
            }

            // Delete the backup file.  Since the backup name is different every time
            // it doesn't matter if the delete doesn't immediately remove the file.
            if (exists)
               locator.FileSystem.DeleteFile(backup);

            return exists;
         }
         finally
         {
            if (deleteTemp)
               locator.FileSystem.DeleteFile(temp);
         }
      }

      /// <summary>
      /// Saves data to the store.
      /// </summary>
      /// <param name="id">The ID.</param>
      /// <param name="data">The data.</param>
      /// <returns>True if the item already existed; false if it was created.</returns>
      public async Task<bool> SaveAsync(TId id, TData data)
      {
         await LockAsync();

         // A key objective is that if something goes wrong while writing
         // the new data then the previous data should be retained.
         // 
         // The deletion of a file may not result in its immediate removal
         // (definitely an issue in Windows, may be in other systems, too).
         // The workaround is to rename the file and then delete the renamed
         // file so the original name will behave as if the file doesn't exist.

         // Do serialization first to get those errors out of the way.
         // If this fails there's nothing to undo since we haven't changed any files.
         var serializedData = serializer.Serialize(data);

         // Get the path to the file.
         var file = locator.GetPath(id);

         // Write the data to a temp file, overwriting if it already exists.
         // If this fails there's nothing to undo since the real file is unchanged.
         var temp = locator.GetTempName(file);
         await locator.FileSystem.WriteAllTextAsync(temp, serializedData);
         var deleteTemp = true;
         Exception error = null;
         var exists = false;
         try
         {
            // If the real file already exists then rename it as a backup.
            // If this fails there's nothing to undo since the rename failed so the real file should be unchanged.
            string backup = null;
            exists = await locator.FileSystem.FileExistsAsync(file);
            if (exists)
            {
               backup = locator.GetTempName(file);
               await locator.FileSystem.MoveFileAsync(file, backup);
            }

            Exception moveError = null;
            try
            {
               // Rename the temp file to become the real file.
               await locator.FileSystem.MoveFileAsync(temp, file);

               deleteTemp = false; // if we get here then there's no need to delete the temp file
            }
            catch (Exception e)
            {
               moveError = e;
            }

            if (moveError != null)
            {
               // Rename the backup file to become the real file again.
               // If this fails then the store would be left in an inconsistent state.
               // This really *should* never happen because if we were able to rename the
               // original to the backup then we should be able to rename it back again.
               if (exists)
                  await locator.FileSystem.MoveFileAsync(backup, file);

               // We've restored the original but still need to report the error
               throw moveError;
            }

            // Delete the backup file.  Since the backup name is different every time
            // it doesn't matter if the delete doesn't immediately remove the file.
            if (exists)
               await locator.FileSystem.DeleteFileAsync(backup);
         }
         catch (Exception e)
         {
            error = e;
         }
         
         if (deleteTemp)
            await locator.FileSystem.DeleteFileAsync(temp);

         if (error != null)
            throw error;

         return exists;
      }

      /// <summary>
      /// Loads data from the store.
      /// </summary>
      /// <param name="id">The ID.</param>
      /// <param name="data">The data or default{T} if it did not exist.</param>
      /// <returns>True if the data was found.</returns>
      public bool Load(TId id, out TData data)
      {
         Lock();

         var path = locator.GetPath(id);

         var exists = locator.FileSystem.FileExists(path);
         data = exists ? serializer.Deserialize(locator.FileSystem.ReadAllText(path)) : default(TData);

         return exists;
      }

      /// <summary>
      /// Loads data from the store.
      /// </summary>
      /// <param name="id">The ID.</param>
      /// <returns>Struct indicating whether data was found and the data.</returns>
      public async Task<StoreLoadResult<TData>> LoadAsync(TId id)
      {
         await LockAsync();

         var path = locator.GetPath(id);

         var exists = await locator.FileSystem.FileExistsAsync(path);
         var data = exists ? serializer.Deserialize(await locator.FileSystem.ReadAllTextAsync(path)) : default(TData);

         return new StoreLoadResult<TData>(exists, data);
      }

      /// <summary>
      /// Gets all available data from the store.
      /// </summary>
      /// <returns>All available data; this may be empty if there was no data.</returns>
      public IEnumerable<TData> LoadMany()
      {
         Lock();

         return locator.EnumerateFiles()
                       .Select(locator.FileSystem.ReadAllText)
                       .Select(serializer.Deserialize);
      }

      /// <summary>
      /// Gets all available data from the store.
      /// </summary>
      /// <returns>All available data; this may be empty if there was no data.</returns>
      public async Task<IEnumerable<TData>> LoadManyAsync()
      {
         await LockAsync();

         var datas = new List<TData>();

         foreach (var file in await locator.EnumerateFilesAsync())
         {
            var contents = await locator.FileSystem.ReadAllTextAsync(file);
            var data = serializer.Deserialize(contents);
            datas.Add(data);
         }

         return datas;
      }

      /// <summary>
      /// Gets all available IDs from the store.
      /// </summary>
      /// <returns>All available IDs; this may be empty if there was no data.</returns>
      public IEnumerable<TId> LoadManyIds()
      {
         Lock();

         return locator.EnumerateFiles()
                       .Select(locator.GetId);
      }

      /// <summary>
      /// Gets all available IDs from the store.
      /// </summary>
      /// <returns>All available IDs; this may be empty if there was no data.</returns>
      public async Task<IEnumerable<TId>> LoadManyIdsAsync()
      {
         await LockAsync();

         return (await locator.EnumerateFilesAsync())
            .Select(locator.GetId);
      }

      /// <summary>
      /// Removes data from the store.
      /// </summary>
      /// <param name="id">The ID.</param>
      /// <returns>True if the data existed.</returns>
      public bool Delete(TId id)
      {
         Lock();

         var file = locator.GetPath(id);

         // The deletion of a file may not result in its immediate removal
         // (definitely an issue in Windows, may be in other systems, too).
         // The workaround is to rename the file and then delete the renamed
         // file so the original name will behave as if the file doesn't exist.

         var exists = locator.FileSystem.FileExists(file);
         if (!exists)
            return false; // File doesn't exist so there's nothing more to do
            
         // Rename to a unique name and then delete the renamed file.
         var backup = locator.GetTempName(file);
         locator.FileSystem.MoveFile(file, backup);
         return locator.FileSystem.DeleteFile(backup);
      }

      /// <summary>
      /// Removes data from the store.
      /// </summary>
      /// <param name="id">The ID.</param>
      /// <returns>True if the data existed.</returns>
      public async Task<bool> DeleteAsync(TId id)
      {
         await LockAsync();

         var file = locator.GetPath(id);

         // The deletion of a file may not result in its immediate removal
         // (definitely an issue in Windows, may be in other systems, too).
         // The workaround is to rename the file and then delete the renamed
         // file so the original name will behave as if the file doesn't exist.

         var exists = await locator.FileSystem.FileExistsAsync(file);
         if (!exists)
            return false; // File doesn't exist so there's nothing more to do

         // Rename to a unique name and then delete the renamed file.
         var backup = locator.GetTempName(file);
         await locator.FileSystem.MoveFileAsync(file, backup);
         return await locator.FileSystem.DeleteFileAsync(backup);
      }
   }
}